Домашнее задание (10 баллов)¶

  1. (2 балла) Закончить реализацию ClassificationDecisionTree в decision_tree (реализовать feature_importance_, проверить корректность predict) и RandomForestClassifier в random_forest (predict/predict_proba). Обратите внимение, что в random_forest в качестве base_estimator предполагается использовать DecisionTreeClassifier из sklearn, использовать вашу реализацию решающего дерева необязательно.
    Запуск тестов
  • python -m unittest discover sem_dt_rf/decision_tree/tests
  • python -m unittest discover sem_dt_rf/random_forest/tests
In [ ]:
 
  1. (1 балл) Для регрессионного дерева необходимо использовать такой критерий: $$H(R) = \min_c \frac{1}{|R|} \sum_{(x_i, y_i) \in R} (y_i - c)^2$$

    Докажите, что минимум H(R) достигается при $c$:

    $$ c = \frac{1}{|R|} \sum_{(x_j, y_j) \in R} y_j$$

Решение¶

Для нахождения минимума найдем первую производную приравняем к нулю, затем вторую и посмотрим на ее знак

  1. $H'_c(R) = -\frac{2}{|R|} \sum (y_i - c) = 0 $
    $ \sum y_i - |R| * c = 0$ так как количества вычитаний $c$ равно количеству элементов в R
    $c = \frac{1}{|R|} \sum y_i$
  2. $H''_c = \frac{2}{|R|} * |R| = 2 > 0$ - следовательно на шаге 1 получили минимум
  1. (3 балла) Реализуйте регрессионное дерево. В качестве критерия необходимо использовать критерий, определённый в пункте 2. В качестве функции выдачи результатов необходимо использовать среднее значение ответов по всем объектам в листе.

    Сгенерируйте однопризнаковую выборку для тестирования дерева и покажите работу дерева на этой выборке (пример см. ниже, можно использовать свою версию). Отобразите на одном графике значения алгоритма и точки. Что меняется при изменении параметра глубины? Сделайте выводы.

In [141]:
import numpy as np
import matplotlib.pyplot as plt
from sem_dt_rf.decision_tree.decision_tree import RegressionDecisionTree
from sklearn.tree import DecisionTreeRegressor
import seaborn as sns
%matplotlib inline
In [142]:
# generate example
x_shape = 300
# features for fit 
x = np.arange(x_shape) / 100
# features for test the fitted model
x_test = np.random.uniform(0, 3.0, 300)#np.arange(x_shape) / 100#np.random.uniform(0, 3, 300)
# values for fit
y = x**3 * np.sin(x**3) + (np.random.random(x_shape)*2 - 1)  * 3 
#values with the correct dependence on x (without error)
y_true_func = x_test**3 * np.sin(x_test**3)

Регрессионое дерево¶

  • Реализовано в виде отдельного класса RegressionDecisionTree в файле decision_tree.py.
  • Визуализация зависимости от max_depth
    График ниже состоит из 4 рисунков для каждого значения глубины дерева. train и test графики соотвествуют, результатам предсказания реализованного регрессионого дерева. На графике train представлено сравнение предсказаний с набором данных x, y, на которых обучалось дерево. На графике test, представлено сравнение значений предсказаний модели на сгенерированном датасете x_test, в котором x расположен случайным образом, а не равномерным на отрезке [0,3], и идеальных значений y : $y = x^3 + sin(x^3)$ (значений без шума, то есть только нужная зависимость). Таким образом график test показывает насколько модель уловила искомую зависимость, и присутсвует ли в модели переобучение, которое проявляется, засчет копирования шума из обучающего датасета x.
    Для сравнения корректности написанной модели и правильного регрессионого дерева, оставшиеся графики sklearn train и sklearn test показывают теже величины, что описаны выше, но для модели DecisionTreeRegressor из бибилотеки sklearn
In [155]:
for i in [4,6,11,12, 30]:
    max_depth = i
    reg_tree = RegressionDecisionTree(max_depth=max_depth, min_leaf_size = 2) 
    reg_tree.fit(x.reshape((-1,1)), y)
    
    skl = DecisionTreeRegressor(max_depth = max_depth)
    skl.fit(x.reshape((-1,1)), y)
    y_pred = reg_tree.predict(x.reshape((-1,1)))
    y_test = reg_tree.predict(x_test.reshape((-1,1)))
    skl_pred = skl.predict(x.reshape((-1,1)))
    skl_test = skl.predict(x_test.reshape((-1,1)))

    fig, ax = plt.subplot_mosaic([['train', 'test'],['sklearn train', 'sklearn test']])
    fig.set_size_inches(18, 12)
    fig.suptitle(f'Max depth ={i} ', fontsize=18)
    
    ax['train'].scatter(x, y, label='y generated')
    ax['train'].scatter(x,y_pred, label = 'y predicted')
    ax['train'].legend(fontsize=14)
    ax['train'].set_title('The result of a tree on the fitting data', fontsize=18)

    ax['test'].scatter(x_test, y_true_func, label='y = x^3 + sin(x^3)')
    ax['test'].scatter(x_test, y_test, label='y predicted')
    ax['test'].legend(fontsize=14)
    ax['test'].set_title('The result of a tree on new generated data', fontsize=18)

    ax['sklearn train'].scatter(x, y, label='y generated')
    ax['sklearn train'].scatter(x,skl_pred, label = 'y predicted')
    ax['sklearn train'].legend(fontsize=14)
    ax['sklearn train'].set_title('The result of a sklearn tree on the fitting data', fontsize=18)

    ax['sklearn test'].scatter(x_test, y_true_func, label='y = x^3 + sin(x^3)')
    ax['sklearn test'].scatter(x_test, skl_test, label = 'sklearn predicted')
    ax['sklearn test'].legend(fontsize=14)
    ax['sklearn test'].set_title('The result of a sklearn tree on new generated data', fontsize=18)


    
    
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Выводы¶

Что ожидаем увидеть?
При малых значениях глубины модель не дообчается и плохо фитирует искомую зависимость
При слишком больших значениях глубины модель переобучается и фитирует все точки, стараясь описать шум.
Что видим
Далее будем смотреть только на графики train и test, показывающие результат для реализованной модели.

  • Начиная с небольших глубин (max_depth = 4) видим, что модели не дообучена
  • На max_depth = 6 появляются ступеньки, но еще слишком широкие
  • max_depth = 11, max_depth = 12, модель уловила основную зависимость, несмотря на небольшие недочеты (отдельные ступеньки x = [1.2 , 1.7])
  • max_depth = 30, переобучение, слишком большое дерево, на графике test видим, что хотя все точки идеально ложатся на кривую, они копируют шум, которого нет в исходной зависимости. (широкая полоса оранжевых точек)
    Вывод
    Глубина должна быть не слишком большой, для избежания переобучения, но и не слишком малой, чтобы глубины дерева хватило для нахождения основной зависимости.
  1. (4 балла) Протестируйте различные реализации random_forest на fetch_covtype датасете (можно загрузить с помощью sklearn.datasets.fetch_covtype). Возможно, поможет ноутбук с семинара ensembles_seminar.ipynb. Для честного сравнения старайтесь использовать похожий набор гиперпараметров.
  • ваша реализация (import RandomForestClassifier as MyRandomForestClassifier ниже)

  • sklearn https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html

  • lightgbm https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMModel.html см. параметр boosting_type

  • xgboost https://xgboost.readthedocs.io/en/stable/tutorials/rf.html

    Что нужно сделать:

  • Разбейте данные на train и test.

  • Оцените качество алгоритмов по метрике (balanced_accuracy_score)[https://scikit-learn.org/stable/modules/generated/sklearn.metrics.balanced_accuracy_score.html]

  • Оцените время работы train и predict

  • Сделайте выводы

ВАЖНО¶

Если ниже не отображаются какие-то графики, то это не отображаются графки plotly.
Их можно найти в html файле в zip архиве, который я прикрепляю на всякий случай

In [2]:
import sys
#sys.path.extend(['']) # change your path
In [3]:
from sem_dt_rf.random_forest.random_forest import RandomForestClassifier as MyRandomForestClassifier
In [4]:
from sklearn.datasets import fetch_covtype
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import balanced_accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.multiclass import OneVsRestClassifier
import lightgbm as lgb
import xgboost as xgb
import pandas as pd
In [5]:
cov_type = fetch_covtype(as_frame = True)
In [6]:
print(cov_type.DESCR)
cov_type_train_columns = cov_type.frame.columns.to_list()
cov_type_train_columns.remove('Cover_Type')
.. _covtype_dataset:

Forest covertypes
-----------------

The samples in this dataset correspond to 30Г—30m patches of forest in the US,
collected for the task of predicting each patch's cover type,
i.e. the dominant species of tree.
There are seven covertypes, making this a multiclass classification problem.
Each sample has 54 features, described on the
`dataset's homepage <https://archive.ics.uci.edu/ml/datasets/Covertype>`__.
Some of the features are boolean indicators,
while others are discrete or continuous measurements.

**Data Set Characteristics:**

    =================   ============
    Classes                        7
    Samples total             581012
    Dimensionality                54
    Features                     int
    =================   ============

:func:`sklearn.datasets.fetch_covtype` will load the covertype dataset;
it returns a dictionary-like 'Bunch' object
with the feature matrix in the ``data`` member
and the target values in ``target``. If optional argument 'as_frame' is
set to 'True', it will return ``data`` and ``target`` as pandas
data frame, and there will be an additional member ``frame`` as well.
The dataset will be downloaded from the web if necessary.

In [7]:
x_train, x_valtest, y_train, y_valtest = train_test_split(cov_type.frame[cov_type_train_columns],
                                                    cov_type.frame[['Cover_Type']], 
                                                    test_size = 0.33
                                                   )
x_val, x_test, y_val, y_test = train_test_split(x_valtest,
                                                    y_valtest, 
                                                    test_size = 0.5
                                                   )

Сравнение реализаций RandomForest¶

План¶

Для сравнения реализаций случайного леса в различных библиотеках нужно впервую очередь подобрать оптимальные параметры для каждой реализации. В ходе подбора параметров я буду руководствоваться следующими принципами с лекций:

  • Самый важный параметр, влияющий на качество случайного леса - max_features (количество или доля признаков используемых при расщеплении). Данный параметр нужно подбирать в первую очередь
  • Второй по важности параметр subsample (подвыборка семплов, используемых для построения дерева). Может изменять точность порядка сотых. Ожидается, что чем ближе значение к 1, тем выше точность, поэтому во всех моделях значение бралось либо равным 1, либо, если модель того не позволяла, очень близкое к 1.
  • Максимальная глубина. Оптимальное значение - не ограничена, строим максимально большое дерево насколько возможно.
  • min_samples_leaf - минимальное число семплов в листе. Так как решаем задачу классфикации берем равное 1 во всех моделях. Оптимизировать не будем. Чем больше значение, тем более консервативна модель, тем хуже точность (проверено на lightgbm, изменение данного параметра на 1 дало выигрыш в две сотых с 0.93 -> 0.95).
  • max_leaves_nodes, min_impurity_gain и тд. могут изменить точность на небольшие значения порядка сотых. Нужно подбирать только в последнюю очередь, если полученные значения скоров для моделей получатся близки.

Из вышесказанного план имеет следующий вид:

  • Подбираю перебором только max_features для каждой модели, при этом беру всю выборку семплов, количество семплов в листе = 1, количество деревьев в ансамбле = 20 (некое разумное значение). Для перебора параметров использую оптюну, потому что есть удобный готовый шаблон и красивая визуализация.
  • Ожидаю что у всех моделей значение max_features приблизительно похожее.
  • Смотрю на скоры моделей.
  • Показываю, что от увеличения кол-ва деревье в лесе (дефолтно выбрал 20) результат не улучшается (значительно)
  • Настраиваю остальные признаки, если необходимо (скоры моделей близки (отличаются но тысячные доли))
  • Делаю вывод.
In [8]:
import optuna
from functools import partial
import numpy as np


def optimize(objective, X_train, y_train, X_val, y_val, 
             n_trials, n_jobs, direction,
             n_startup_trials, gamma, n_ei_candidates, multivariate
            ):
    tpe_sampler = optuna.samplers.TPESampler(
        n_startup_trials=n_startup_trials, 
        gamma=gamma,
        n_ei_candidates=n_ei_candidates,
        multivariate=multivariate
    )
    study = optuna.create_study(sampler=tpe_sampler, direction=direction)
    if X_val is None or y_val is None:
        obj_func = partial(objective, X_train=X_train, y_train=y_train)
    else:
        obj_func = partial(objective, X_train=X_train, X_val=X_val, y_train=y_train, y_val=y_val)
    study.optimize(obj_func, n_trials=n_trials, n_jobs=n_jobs)
    return study

Подбор max_features для Random Forest¶

Выбираем некое разумное значение количества деревьев : 20,
Не ограничиваем деревья по глубине.
И так как задача классификации количество семплов в листе начинается от 1.
Для решения задачи классификации нескольких классов (7), используем OneVsRestClassifier.

In [71]:
# sklearn RF
def objective_RFC(
    trial: optuna.trial.Trial, # нечто оптюновское
    X_train, X_val, y_train, y_val # данные
):

    model = OneVsRestClassifier(RandomForestClassifier(
        n_estimators = 20,#trial.suggest_int('n_estimators', 10, 150, log=True),
        max_depth = None, #trial.suggest_int('max_depth', 10, 50),
        #min_samples_split = 1, #trial.suggest_int('min_samples_split', 5, 100),
        min_samples_leaf = 1, # trial.suggest_int('min_samples_leaf', 5, 100),
        max_features =  trial.suggest_int('max_features', 5, 54) ,
        #max_leaf_nodes = trial.suggest_int('max_leaf_nodes', 5, 300, log = True),
        #min_impurity_decrease = trial.suggest_float('min_impurity_decrease', 0, 0.1),
        #bootstrap = True,
        #max_samples =  trial.suggest_float('max_samples', 0.5, 1, step = 0.1)
        
    ))
    
    model.fit(X_train, y_train)
    y_predict = model.predict_proba(X_val).argmax(axis=1) + 1
    score = balanced_accuracy_score(y_val, y_predict)
    return score


params_RFC = dict(
    objective = objective_RFC,
    X_train = x_train,
    y_train = y_train,
    X_val = x_val,
    y_val = y_val,
    n_trials = 49,
    n_jobs = 15,
    direction = 'maximize',
    n_startup_trials = 49 ,
    gamma = lambda n_trials: min(int(np.ceil(0.1 * n_trials)), 25),
    n_ei_candidates = 5,
    multivariate = True
)
In [ ]:
study_RFC = optimize(**params_RFC)
In [73]:
# How to look at study
from optuna.visualization import plot_slice
# the func below is needed to choose the parts to see
def my_clipper(trial: optuna.trial.FrozenTrial):
    score = trial.value
    return min(1.1, score)
plot_slice(study_RFC) #, target= my_clipper)
# Нужно max features и все, остальное дает заметно меньшие справления
# max_features = 26, не лучший, но бьет 0.92 и скорость важна
# Лучшее 45

Лучшее значение количества features приблизительно 45 (из 54). Если очень постараться можно рассмотреть унимодальную зависимость с пиком в данной области. Для дальнейших сравнений удобно запомнить значение max_features в относительном виде:
max_features = 0.83

Multiclass для myRFC¶

Как будет видно далее myRandomForestClassifier занимает слишком много времени при фитировании, поэтому присвоим ему такое же значение глубины как и для sklearn RandomForest, полученное в предыдущих ячейках.
Для того чтобы предсказывать несколько классов на реализованном myRandomForestClassifier, можно применить OneVsRestClassifier или дать возможность ансамблю самому пытаться предсказать несколько классов. Для реализации первого более правильного варианта необходимо реализовать get_params метод, который описан в random_forest.py
Ниже приведено сравнение двух подходов

In [23]:
model_myRFC =  MyRandomForestClassifier(
        n_estimators=20,
        max_objects_samples=1,
        max_features_samples=45/54,
        #max_depth= 40,
        #min_samples_leaf=10,  
    )

model_myRFC.fit(x_train.to_numpy(), y_train.to_numpy())
y_predict_myRFC = model_myRFC.predict(x_val.to_numpy())
score = balanced_accuracy_score(y_val-1, y_predict_myRFC)
print('No OneVsRestClassifier', score)
0.7437730120814622
In [24]:
model_myRFC =  OneVsRestClassifier(MyRandomForestClassifier(
        n_estimators=20,
        max_objects_samples=1,
        max_features_samples=45/54,
        #max_depth= 40,
        #min_samples_leaf=10,  
    ))

model_myRFC.fit(x_train.to_numpy(), y_train.to_numpy())
y_predict_myRFC = model_myRFC.predict(x_val.to_numpy())
score = balanced_accuracy_score(y_val, y_predict_myRFC)
print('With OneVsRestClassifier', score)
0.7965172278958403

Подбор max_features для lightgbm¶

Для реализации RandomForest в ligthgbm необходимы следующие параметры:

  • boosting_type = 'rf',
  • learning_rate = 1,
  • subsample_freq = 1,

Мультиклассовость включена в lightgbm. Для ее использования необходимо:

  • objective = 'multiclass',
  • num_class = 7,

Так как при subsample = 1 выдается ошибка, то возьмем очень большое значение близкое к 1 : 0.99999

max_depth = -1 - означает, что нет ограничений по глубине. num_leaves = 2^10, - в идеале хотели бы не иметь ограничений на количество листьев, так как хотим построить максимально глубокие деревья. Однако такого параметра в модуле нет, так что выставляем очень большие количества листьев (максимальное значение). Для поиска max_depth возьмем 2^10, так как при большем значении переполняется память. Но при сравнении и тестировании будем использовать 2^15, чем больше листьев тем лучше точность (только в рандомных лесах)

In [12]:
# lightgbm
def objective_lightgbm(
    trial: optuna.trial.Trial, # нечто оптюновское
    X_train, X_val, y_train, y_val # данные
):

    model = lgb.LGBMModel(
        boosting_type = 'rf',
        objective = 'multiclass',
        subsample_freq = 1,
        subsample = 0.99999,
        colsample_bytree = trial.suggest_float('max_features', 4/54, 1, step = 0.03 ),
        max_depth = -1,
        num_class = 7,
        n_estimators = 20,
        num_leaves = 2**10,
        learning_rate = 1,
        min_child_samples = 1,
    )
    
    model.fit(X_train, y_train)
    y_predict = model.predict(X_val).argmax(axis=1)#model.predict_proba(X_val).argmax(axis=1) + 1
    score = balanced_accuracy_score(y_val, y_predict)
    return score


params_lightgbm = dict(
    objective = objective_lightgbm,
    X_train = x_train,
    y_train = y_train-1,
    X_val = x_val,
    y_val = y_val-1,
    n_trials = 30,
    n_jobs = 15,
    direction = 'maximize',
    n_startup_trials = 30 ,
    gamma = lambda n_trials: min(int(np.ceil(0.1 * n_trials)), 25),
    n_ei_candidates = 5,
    multivariate = True
)
In [ ]:
study_lightgbm = optimize(**params_lightgbm)
In [14]:
from optuna.visualization import plot_slice
# the func below is needed to choose the parts to see
def my_clipper(trial: optuna.trial.FrozenTrial):
    score = trial.value
    return min(1.1, score)
plot_slice(study_lightgbm)

Получили оптимальное значение при max_features = 0.824, во первых опять имеется унимодальная зависимость, а во вторых результат сходится с RandomForest из sklearn

In [15]:
# Проверка результата с тестовым значением num_leaves
model_lgbm = lgb.LGBMModel(
        boosting_type = 'rf',
        objective = 'multiclass',
        subsample_freq = 1,
        subsample = 0.99999,
        colsample_bytree = 0.82,
        max_depth = -1,
        num_class = 7,
        n_estimators = 20,
        num_leaves = 2**15,
        learning_rate = 1,
        #min_child_samples = 1,
        #num_parallel_tree = 100,
    )
model_lgbm.fit(x_train, y_train-1)
y_predict_lgbm = model_lgbm.predict(x_val).argmax(axis=1)#model.predict_proba(X_val).argmax(axis=1) + 1
score = balanced_accuracy_score(y_val-1, y_predict_lgbm)
print(score)
[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.001626 seconds.
You can set `force_row_wise=true` to remove the overhead.
And if memory is not enough, you can set `force_col_wise=true`.
[LightGBM] [Info] Total Bins 2263
[LightGBM] [Info] Number of data points in the train set: 389278, number of used features: 53
[LightGBM] [Info] Start training from score -1.007974
[LightGBM] [Info] Start training from score -0.718560
[LightGBM] [Info] Start training from score -2.789077
[LightGBM] [Info] Start training from score -5.349108
[LightGBM] [Info] Start training from score -4.121841
[LightGBM] [Info] Start training from score -3.509073
[LightGBM] [Info] Start training from score -3.346168
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
0.9351298964757427

Подбор max_features для xgboost¶

Здесь реализация случайного леса завязана на "num_parallel_tree", "booster" : 'gbtree', Заметим, что в RandomForest на xgboost нет параметра отождетсвляющегося с минимальным количеством семплов в листе. Наиболее близким аналогом является min_child_weight. Его пробуем оптимизировать вместе с max_features. (ожидаю что он окажется около 0)

In [20]:
# xgboost
def objective_xgb(
    trial: optuna.trial.Trial, # нечто оптюновское
    X_train, X_val, y_train, y_val # данные
):

    params = {
      #"colsample_bynode": 45/54,
      "booster" : 'gbtree',
      "learning_rate": 1,
      "max_depth": 0,
      "num_parallel_tree": 20,
      "objective": "multi:softmax",
      "subsample": 0.9999,
      "tree_method": "hist",
      "device": "cpu",
      "verbosity" : 2,
      "n_estimators" : 1,
    }
    
    model = xgb.XGBClassifier(
       **params,
       colsample_bynode = trial.suggest_float('max_features', 4/54, 1, step = 0.03 ),
       min_child_weight = trial.suggest_float('min_child_weight', 0, 2 )
               
    )
    
    model.fit(X_train, y_train)
    y_predict = model.predict(X_val)#.argmax(axis=1)#model.predict_proba(X_val).argmax(axis=1) + 1
    score = balanced_accuracy_score(y_val, y_predict)
    return score


params_xgb = dict(
    objective = objective_xgb,
    X_train = x_train,
    y_train = y_train-1,
    X_val = x_val,
    y_val = y_val-1,
    n_trials = 80,
    n_jobs = 3,
    direction = 'maximize',
    n_startup_trials = 60 ,
    gamma = lambda n_trials: min(int(np.ceil(0.1 * n_trials)), 25),
    n_ei_candidates = 5,
    multivariate = True
)
In [ ]:
study_xgb = optimize(**params_xgb)
In [22]:
from optuna.visualization import plot_slice
# the func below is needed to choose the parts to see
def my_clipper(trial: optuna.trial.FrozenTrial):
    score = trial.value
    return min(1.1, score)
plot_slice(study_xgb)

Здесь начиная с 0.65 до 0.85 значение точности выходит на плато, кроме того , при минимальном ограничении на вес ребенка равное 0.2, наблюдается максимальный скор. Причем данно значение оказалось не нулем, хоть и отличается на несколько тысячных. (маленькое ограничение по весу получается работает как защита от небольшого переобучения, не давая создать совсем бесполезные листья)
В итоге для сравнения будем испольщовать значение 0.85 для max_features и значение 0.2 для веса. 0.85 близко к параметрам max_features у lightgbm и RFC, поэтому выбираем его (хоть оно и не максимум; отличие всего в несколько тысячных так что принебрегаем)

max_features около 0.8 для всех моеделей¶

Ниже смотрим на результат всех моделей и визуализируем¶

In [26]:
import time


models = {
    "RFC" : OneVsRestClassifier(RandomForestClassifier(
                n_estimators = 20,
                max_depth = None, 
                min_samples_leaf = 1,
                max_features =  45 ,
            )),
    "myRFC" : OneVsRestClassifier(MyRandomForestClassifier(
                n_estimators=20,
                max_objects_samples=1,
                max_features_samples=0.83,
            )),
    "lightgbm" : lgb.LGBMModel(
                boosting_type = 'rf',
                objective = 'multiclass',
                subsample_freq = 1,
                subsample = 0.9999,
                colsample_bytree = 0.82,
                max_depth = -1,
                num_class = 7,
                n_estimators = 20,
                num_leaves = 2**15,
                learning_rate = 1,
                min_child_samples = 1,
            ),
    "xgboost" : xgb.XGBClassifier(
                booster = 'gbtree',
                colsample_bynode = 0.85,
                min_child_weight = 0.2,
                learning_rate = 1,
                max_depth = 0,
                num_parallel_tree = 20,
                objective = "multi:softmax",
                subsample = 0.9999,
                tree_method = "hist",
                device = "cpu",
                verbosity = 2,
                n_estimators = 1,
                n_jobs = 3
            )  
}

#models_analis = {}

x_train_np = x_train.to_numpy()
y_train_np = y_train.to_numpy() - 1 
x_test_np = x_test.to_numpy()
y_test_np = y_test.to_numpy() - 1

for model_name, model in models.items():
    if model_name in ["RFC", "myRFC", "lightgbm"]: # adjust !
        continue
    print(f"Start fitting {model_name}")
    start_fit = time.time()
    model.fit(x_train_np, y_train_np)
    end_fit = time.time()
    print(f"End fitting {model_name}")

    print(f"Start predicting {model_name}")
    start_predict = time.time()
    if model_name == "lightgbm":
        y_predict = model.predict(x_test_np).argmax(axis=1)
    else:
        y_predict = model.predict(x_test_np)
    end_predict = time.time()
    print(f"End predicting {model_name}")
    
    score = balanced_accuracy_score(y_test_np, y_predict)

    models_analis[model_name] = {
        'score' : score,
        'fit_time' : end_fit - start_fit,
        'predict_time' : end_predict - start_predict
    }
Start fitting xgboost
End fitting xgboost
Start predicting xgboost
End predicting xgboost
In [27]:
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

models_frame = pd.DataFrame(models_analis).T
models_frame.index.rename('models', inplace= True )
fig, ax = plt.subplot_mosaic([['score'], ['fit time'], ['predict time']])
fig.set_size_inches(13, 15)
ax['score'].set_xticks(np.arange(0, 20, 1)/20)
ax['score'].grid()
ax['score'].set_title('Score')
sns.barplot(models_frame, y = 'models', x = 'score', ax=ax['score'], width = 0.4 , hue = 'models', fill = True, orient = 'h')
ax['fit time'].set_xticks(np.arange(0, 400, 20))
ax['fit time'].grid()
ax['fit time'].set_title('Fit time in seconds')
sns.barplot(models_frame, y = 'models', x = 'fit_time', ax=ax['fit time'], width = 0.4 , hue = 'models', fill = True, orient = 'h')
ax['predict time'].set_xticks(np.arange(0, 4, 0.2))
ax['predict time'].grid()
ax['predict time'].set_title('Predict time in seconds')
sns.barplot(models_frame, y = 'models', x = 'predict_time', ax=ax['predict time'], width = 0.4 , hue = 'models', fill = True, orient = 'h')
plt.show()
No description has been provided for this image
In [28]:
models_frame
Out[28]:
score fit_time predict_time
models
RFC 0.925829 242.541658 1.297809
myRFC 0.887439 360.507245 3.902016
lightgbm 0.954323 130.361382 0.277964
xgboost 0.905462 29.415183 0.486007

Выводы¶

  1. Собственноручно реализованная модель работает хуже и дольше всех остальных.
  2. xgboost работает хуже lightgbm, RFC. Это может объясняться тем, что из всех модулей он меньше всего приспособен для реализации в виде случайного леса. Например, как замечалось выше, в нем отстуствуют параметры отвественные за split nodes по количеству семплов или за минимальное количество семплов в листе. С другой стороны он выигрывает по скорости фитирования, причем выигрывает у lighgbm. Я бы объяснил это тем, что в lighgbm больше листьев (там для каждого семпла есть лист - min_leaf_samples = 1), тогда как у xgboost есть подорбанное нами ограничение в 0.2 для минимального веса. В подверждение этих слов ниже график сравнения скорости фитирования с минимальным весом 0 у xgboost
  3. RFC на 0.02 обгоняет xgboost, при том, что в нем не оптимизировали min_gain и остальные параметры, которые могли добавить еще несколько тысячных к скору RFC. Следовательно отрыв по скору связан не с настройкой параметров, а с внутреннем устройством модели (лучшая реализация или что-то что спрятано внутри)
  4. ligtgbm как и ожидалось значительно обгоняет всех по score и по времени работы, кроме fit, о чем говорилось выше.

Зависимость моделей от количества деревьев¶

In [ ]:
# dependence on trees number
# evrywhere take only subsample = 0.5 for speed
n_trees = [3,6,9,12,15,18,20,23,26,29,35]
#RFC

RFC_scores = []
for n_t in n_trees:
    print(f"RFC with {n_t} trees is processed")
    model = OneVsRestClassifier(RandomForestClassifier(
                n_estimators = n_t,
                max_depth = None, 
                min_samples_leaf = 1,
                max_features =  45 ,
                bootstrap = True,
                max_samples = 0.5
            ))
    model.fit(x_train_np, y_train_np)
    y_predict = model.predict(x_test_np)
    score = balanced_accuracy_score(y_test_np, y_predict)
    RFC_scores.append(score)


myRFC_scores = []
for n_t in n_trees:
    print(f"myRFC with {n_t} trees is processed")
    model =  OneVsRestClassifier(MyRandomForestClassifier(
                n_estimators=n_t,
                max_objects_samples=0.5,
                max_features_samples=0.83
            ))
    model.fit(x_train_np, y_train_np)
    y_predict = model.predict(x_test_np)
    score = balanced_accuracy_score(y_test_np, y_predict)
    myRFC_scores.append(score)


lightgbm_scores = []
for n_t in n_trees:
    print(f"lightgbm with {n_t} trees is processed")
    model =  lgb.LGBMModel(
               boosting_type = 'rf',
                objective = 'multiclass',
                subsample_freq = 1,
                subsample = 0.5,
                colsample_bytree = 0.82,
                max_depth = -1,
                num_class = 7,
                n_estimators = n_t,
                num_leaves = 2**15,
                learning_rate = 1,
                min_child_samples = 1,
    )
    model.fit(x_train_np, y_train_np)
    y_predict = model.predict(x_test_np).argmax(axis=1)
    score = balanced_accuracy_score(y_test_np, y_predict)
    lightgbm_scores.append(score)


xgb_scores = []
for n_t in n_trees:
    print(f"xgb with {n_t} trees is processed")
    model =   xgb.XGBClassifier(
                booster = 'gbtree',
                colsample_bynode = 0.85,
                min_child_weight = 0.2,
                learning_rate = 1,
                max_depth = 0,
                num_parallel_tree = n_t,
                objective = "multi:softmax",
                subsample = 0.5,
                tree_method = "hist",
                device = "cpu",
                verbosity = 2,
                n_estimators = 1,
                n_jobs = 3
            )  
    model.fit(x_train_np, y_train_np)
    y_predict = model.predict(x_test_np)
    score = balanced_accuracy_score(y_test_np, y_predict)
    xgb_scores.append(score)
    
In [45]:
fig, ax = plt.subplot_mosaic([['n_trees']])
ax['n_trees'].grid()
sns.scatterplot(x = n_trees, y = RFC_scores, ax= ax['n_trees'], label = 'RFC')
sns.scatterplot(x = n_trees, y = myRFC_scores, ax= ax['n_trees'], label = 'myRFC')
sns.scatterplot(x = n_trees, y = lightgbm_scores, ax= ax['n_trees'], label = 'lightgbm')
sns.scatterplot(x = n_trees, y = xgb_scores, ax= ax['n_trees'], label = 'xgb')
ax['n_trees'].legend(loc='lower left')
ax['n_trees'].set_xlabel("trees number")
ax['n_trees'].set_ylabel("score")
ax['n_trees'].set_title('Dependence on trees number')


#ax['n_trees'].scatter(n_trees, RFC_scores)
#ax['n_trees'].scatter(n_trees, myRFC_scores)
#ax['n_trees'].scatter(n_trees, lightgbm_scores)
#ax['n_trees'].scatter(n_trees, xgb_scores)
plt.show()
No description has been provided for this image

Вывод¶

В довершение вывода выше, факт использования конкретного значения 20 - параметра количества деревьев не влияете на точность исследования, так как начиная с 15 деревьев качество увеличивается у всех моделей и очень плавно(слабо, нормальное поведение для случайного леса, чем больше деревьев тем лучше). Так что оптимизацию и исследование можно было проводить с любым количеством деревьев в лесе, начиная со значения 15

In [ ]: